package org.little.aop.framework;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.little.aop.Advice;
import org.little.aop.AopConfigException;
import org.little.util.Assert;
import org.springframework.cglib.core.CodeGenerationException;
import org.springframework.cglib.core.SpringNamingPolicy;
import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.CallbackFilter;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;

/**
 * @author Tinyspot
 * @date 2022-05-29 21:45
 */
public class CglibProxyFactory implements AopProxyFactory {
    protected static final Log logger = LogFactory.getLog(CglibProxyFactory.class);

    private static final int AOP_PROXY = 0;
    private static final int INVOKE_TARGET = 1;
    private static final int NO_OVERRIDE = 2;
    private static final int DISPATCH_TARGET = 3;
    private static final int DISPATCH_ADVISED = 4;
    private static final int INVOKE_EQUALS = 5;
    private static final int INVOKE_HASHCODE = 6;

    protected final AopConfig config;
    private Object[] constructorArgs;
    private Class<?>[] constructorArgTypes;

    public CglibProxyFactory(AopConfig config) throws AopConfigException {
        Assert.notNull(config, "AdvisedSupport must not be null");
        if (config.getAdvices().size() == 0) {
            throw new AopConfigException("No advisors and no TargetSource specified");
        }
        this.config = config;
    }

    public void setConstructorArguments(Object[] constructorArgs, Class<?>[] constructorArgTypes) {
        if (constructorArgs == null || constructorArgTypes == null) {
            throw new IllegalArgumentException("Both 'constructorArgs' and 'constructorArgTypes' ");
        }
        if (constructorArgs.length != constructorArgTypes.length) {
            throw new IllegalArgumentException("Number of 'constructorArgs' (" + constructorArgs.length + ") must match number of 'constructorArgTypes' (" + constructorArgTypes.length + ")");
        }
        this.constructorArgs = constructorArgs;
        this.constructorArgTypes = constructorArgTypes;
    }


    @Override
    public Object getProxy() {
        return getProxy(null);
    }

    @Override
    public Object getProxy(ClassLoader classLoader) {

        if (logger.isDebugEnabled()) {
            logger.debug("Creating CGLIB proxy: target source is " + this.config.getTargetClass());
        }
        try {
            Class<?> rootClass = this.config.getTargetClass();

            Enhancer enhancer = new Enhancer();
            if (classLoader != null) {
                enhancer.setClassLoader(classLoader);
            }
            enhancer.setSuperclass(rootClass);

            enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
            enhancer.setInterceptDuringConstruction(false);

            Callback[] callbacks = getCallbacks(rootClass);
            Class<?>[] types = new Class<?>[callbacks.length];
            for (int i = 0; i < types.length; i++) {
                types[i] = callbacks[i].getClass();
            }

            enhancer.setCallbackFilter(new ProxyCallbackFilter(this.config));
            enhancer.setCallbackTypes(types);
            enhancer.setCallbacks(callbacks);

            // Object proxy;
            // if (this.constructorArgs != null) {
            //     proxy = enhancer.create(this.constructorArgTypes, this.constructorArgs);
            // } else {
            //     proxy = enhancer.create();
            // }
            // return proxy;
            return enhancer.create();
        } catch (CodeGenerationException e) {
            throw new AopConfigException("Could not generate CGLIB subclass of class [" + this.config.getTargetClass() + "]: " + "Common causes of this problem include using a final class or a non-visible", e);
        } catch (IllegalArgumentException e) {
            throw new AopConfigException("Could not generate CGLIB subclass of class [" + this.config.getTargetClass() + "]: " + "Common causes of this problem include using a final class or a non-visible", e);
        } catch (Exception e) {
            throw new AopConfigException("Unexpected Aop exception", e);
        }
    }

    private Callback[] getCallbacks(Class<?> rooClass) {
        Callback aopInterceptor = new DynamicAdvisedInterceptor(this.config);
        Callback[] callbacks = new Callback[] {
                aopInterceptor,  // AOP_PROXY for normal advice
                /*targetInterceptor,  // INVOKE_TARGET invoke target without considering advice, if optimized
                new SerializableNoOp(),  // NO_OVERRIDE  no override for methods mapped to this
                targetDispatcher,        //DISPATCH_TARGET
                this.advisedDispatcher,  //DISPATCH_ADVISED
                new EqualsInterceptor(this.advised),
                new HashCodeInterceptor(this.advised)*/
        };

        return callbacks;
    }

    private static class ProxyCallbackFilter implements CallbackFilter {

        private final AopConfig config;

        public ProxyCallbackFilter(AopConfig config) {
            this.config = config;
        }

        @Override
        public int accept(Method method) {
            // 此处做了简化
            return AOP_PROXY;
        }
    }

    private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable {

        private final AopConfig config;

        public DynamicAdvisedInterceptor(AopConfig config) {
            this.config = config;
        }

        @Override
        public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            Class<?> targetClass = null;

            Object target = null;
            target = this.config.getTargetObject();
            if (target != null) {
                targetClass = target.getClass();
            }
            List<Advice> chain = this.config.getAdvices(method/*, targetClass*/);
            Object retVal;
            if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
                retVal = methodProxy.invoke(target, args);
            } else {
                List<org.aopalliance.intercept.MethodInterceptor> interceptors = new ArrayList<>();
                interceptors.addAll(chain);
                retVal = new ReflectiveMethodInvocation(target, method, args, interceptors).proceed();
            }
            return retVal;
        }
    }
}
